﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using ThreeByte.ActiveDeck.Publish.Model;
using ThreeByte.ActiveDeck.Service;

//Based Heavily on http://laharsub.codeplex.com

namespace ThreeByte.ActiveDeck.Publisher.Client
{
    class HttpLongPollManager
    {
        private static readonly XNamespace ns = ServiceConfiguration.NAMESPACE;

        bool pollAborted;
        HttpWebRequest poll;
        Dictionary<string, Subscription> subscriptions;
        string baseAddress;
        object syncRoot = new object();
        AsyncCallback onPollResponse;
        string connectionGroupName;
        private NetworkCredential Credential;

        public HttpLongPollManager(string baseAddress, NetworkCredential cred)
        {
            this.baseAddress = baseAddress;
            this.subscriptions = new Dictionary<string, Subscription>();
            this.onPollResponse = new AsyncCallback(this.OnPollResponse);
            this.connectionGroupName = Guid.NewGuid().ToString();
            this.Credential = cred;
        }

        void AbortPoll()
        {
            if (this.poll != null)
            {
                lock (this.syncRoot)
                {
                    if (this.poll != null)
                    {
                        this.pollAborted = true;
                        this.poll.Abort();
                        this.poll = null;
                    }
                }
            }
        }

        public void AddSubscription(Subscription s)
        {
            lock (this.syncRoot)
            {
                if (this.subscriptions.ContainsKey(s.Channel))
                {
                    throw new InvalidOperationException("Cannot create subscription. Only one subscription to a given channel may be active at a time.");
                }
                this.subscriptions[s.Channel] = s;
                this.AbortPoll();
                this.StartPoll();
            }
        }

        public void RemoveSubscription(string channel)
        {
            lock (this.syncRoot)
            {
                if(this.subscriptions.ContainsKey(channel))
                {
                    this.subscriptions.Remove(channel);
                    if (this.subscriptions.Count == 0)
                    {
                        this.AbortPoll();
                    }
                }
            }
        }

        public void RemoveAllSubscriptions()
        {
            lock (this.syncRoot)
            {
                this.AbortPoll();
                this.subscriptions.Clear();
            }
        }

        void FaultAllSubscriptions(Exception e)
        {
            lock (this.syncRoot)
            {
                Exception e1 = new InvalidOperationException("HTTP long poll returned an error. Subscription is terminated.", e);
                foreach (Subscription s in this.subscriptions.Values)
                {
                    if (s.OnError != null)
                    {
                        try
                        {
                            s.OnError(s, e1);
                        }
                        catch (Exception)
                        {
                            // empty
                        }
                    }
                }
                this.subscriptions.Clear();
                this.AbortPoll();
            }
        }

        void StartPoll()
        {

            XElement rootNode = new XElement(ns + "ActiveUpdate");
            XElement subscribeNode = new XElement(ns + "Subscribe");

            rootNode.Add(AuthorizationUtil.ToAuthenticationXML(Credential));
            foreach (Subscription s in this.subscriptions.Values)
            {
                XElement newChannel = new XElement(ns + "Channel", new XAttribute("ID", s.Channel), new XAttribute("From", s.From));
                subscribeNode.Add(newChannel);
            }
            rootNode.Add(subscribeNode);
            this.pollAborted = false;
            this.poll = (HttpWebRequest)WebRequest.Create(string.Format("{0}subscribe", this.baseAddress));
            this.poll.Method = "POST";
            this.poll.ContentType = "text/xml";


#if SILVERLIGHT
#else
            this.poll.Pipelined = false;
            this.poll.ConnectionGroupName = this.connectionGroupName;
#endif

            this.poll.BeginGetRequestStream(ProcessGetRequestStream, rootNode);

        }

        void ProcessGetRequestStream(IAsyncResult result) {
            lock(this.syncRoot) {
                if(this.poll != null && !this.pollAborted) {
                    Stream reqStream = null;
                    try {
                        reqStream = this.poll.EndGetRequestStream(result);
                        XElement subscribeNode = (XElement)(result.AsyncState);
                        string subData = subscribeNode.ToString();
                        byte[] stringArray = Encoding.UTF8.GetBytes(subData);

                        reqStream.Write(stringArray, 0, stringArray.Length);
                        reqStream.Close();
                        reqStream = null;
                        this.poll.BeginGetResponse(this.onPollResponse, null);

                    } catch(Exception e) {
                        this.FaultAllSubscriptions(e);
                    } finally {
                        if(reqStream != null) {
                            reqStream.Close();
                        }
                    }

                }
            }
            
        }

        void OnPollResponse(IAsyncResult result)
        {            
            lock (this.syncRoot) // prevent race condition with OnAbort
            {
                if (this.poll != null && ! this.pollAborted)
                {
                    bool success = false;
                    HttpWebResponse response = null;
                    try
                    {
                        response = (HttpWebResponse)this.poll.EndGetResponse(result);
                        Stream stream = response.GetResponseStream();
                        // TODO: implement asynchronous multipart/mixed parsing without response buffering
                        string body = this.ReadEntireStream(stream);
                        if (!string.IsNullOrEmpty(body))
                        {
                            this.ParseMessages(body);
                        }
                        success = true;
                    }
                    catch (Exception e)
                    {
                        this.FaultAllSubscriptions(e);
                    }
                    finally
                    {
                        this.poll = null;
                        if (response != null)
                        {
                            response.Close();
                        }
                    }
                    if (success && this.subscriptions.Count > 0)
                    {
                        this.StartPoll();
                    }
                }
            }
        }

        private void ParseMessages(string body) {

            XElement messages = XElement.Parse(body);

            foreach(XElement m in messages.Elements(ns + "Message")) {
                DispatchMessage(ActiveMessage.FromXml(m));
            }

        }

        string ReadEntireStream(Stream s)
        {
            StringBuilder sb = new StringBuilder();
            byte[] buffer = new byte[256];
            int i;
            while ((i = s.Read(buffer, 0, buffer.Length)) > 0)
            {
                sb.Append(Encoding.UTF8.GetString(buffer, 0, i));
            }
            return sb.ToString();
        }

        // TODO: should callback be dispatched on a separate thread? Dispatching on the poll response thread slows down issuance of next poll.
        void DispatchMessage(ActiveMessage message)
        {
            Subscription s = null;
            if (this.subscriptions.TryGetValue(message.Channel, out s))
            {
                if (s.From <= message.MessageId)
                {
                    s.From = message.MessageId + 1;
                }
                try
                {
                    if (s.OnMessageReceived != null)
                    {
                        s.OnMessageReceived(s, message);
                    }
                }
                catch (Exception)
                {
                    // empty
                }
            }
        }
    }
}
